<?php
/**
 * MongoDB类，
 * 封装所有相关的MongoDB操作
 */

/*
 * 日志开关, 记录错误 SQL 语句到日志文件中
 *
 * 如在包含此文件前 define('DB_OP_ERROR_LOG', '/tmp/db.error.log')
 * 那么在所有请求中出错的SQL语句都会被记录到此文件中
 *
 * 如果不需要记录日志, 则在包含些文件前 define('DB_OP_ERROR_LOG', false)
 *
 */
defined('DB_OP_ERROR_LOG') || define('DB_OP_ERROR_LOG', RUNTIME_PATH.'db.error.'.date("Y-m-d").'.log');

class MongoOp {

	private $total;
	private $query_res;
	private $conn;

	// SQL 执行时间记录
    private $begin_time;

    // 是否显示调试信息 如果启用会在日志文件记录sql语句
    public $debug = false;

    // DB连接配置信息
    private $db_info = array();

    // 数据库连接列表
    private $conn_list = array();

    // 保持最后的一个操作data
    private $data = '';

    // 保存最近的一条SQL语句
    private $sql = '';

    // 是否已经连接数据库
    private $connected = false;

    // 当前连接ID
    private $mongo = null;

    // db
    private $db = null;

    // collection
    private $collection = null;

    // 上一次的db名称
    private $last_db = '';

    // 上一次的collection 名称
    private $last_collection = '';

    // 错误消息
    private $error = '';

    /**
	 * 构造器
	 */
	public function __construct() {
		//
	}

	/**
     * 取得Mongodb数据库类实例
     *
     * Mongodb只支持类MySQL的基础查询，形如：
     * 1. select xxx from table where xxx order by xxx limit xxx
     * 2. update table set xxx where xxx
     * 3. insert into table values xxx
     * 4. delete from table where xxx
     *
     * @param array ops, $ops = array('cfg'=>DbDispatcher::dispatchTable('ips', '', 'w'));
     * @param array data, DB操作相关的data
     * 	array(
     * 		'fields'  => array(...), /// 可选，不设置或者为空(空数组)表示返回全部字段
     * 		'where'   => array(...), /// 条件
     * 		'orderby' => array('id'=>'asc', 'update_time'=>'desc'), /// 排序条件
     * 		'limit'   => array(0, 10), /// limit 开始位置，pagesize，如果是array(10)，表示0,10
     * 		'sets'    => array(...), /// 更新时需要用到
     * 	)
     *
     * @static
     * @access public
     *
     * @return mixed 返回db_op实例
     */
    public static function getInstance() {
        $args = func_get_args();
        // 这里不能直接调用get_instance_of的自动方法调用
        $db = get_instance_of(__CLASS__, '', '');
        call_user_func_array(array(&$db, 'factory'), $args);

        return $db;
    }

    /**
     * 加载数据库 支持配置文件
     *
     * @access public
     *
     * @param mixed $db_config 数据库配置信息
     *
     * @return string
     *
     * @throws AppException
     */
    public function factory($ops, $data) {

        if (!is_array($data)) $data = array();

        // 得到DB当前操作表的连接信息
		$this->db_info = $this->parse_dbcfg($ops['cfg']);
		$this->db_info['link_key'] = to_guid_string($this->db_info['dsn']); /// .'-'.$this->db_info['db'].'-'.$this->db_info['collection']

		if (empty($data)) $this->data = array();
		else $this->data = $data;

		// 是否开启调试
		if (C('APP_DEBUG')) $this->debug = true;
    }

    /**
     * 连接数据库方法
     *
     * @access public
     *
     * @throws AppException
     */
    public function connect($db_info='') {

    	// 没有传入db连接信息，直接使用默认连接信息
    	if (empty($db_info)) $db_info = $this->db_info;
    	else $db_info = $this->parse_dbcfg($db_info);

    	if (!isset($db_info['link_key'])) $db_info['link_key'] = to_guid_string($db_info['dsn']); /// .'-'.$db_info['db'].'-'.$db_info['collection']

    	if (!isset($this->conn_list[$db_info['link_key']])) {

    		try {
    			if (IS_CLI) {
    				$mongo = new Mongo($db_info['dsn'], array('timeout'=>1000000, 'persist'=>md5($db_info['dsn'])));
    			}
    			else {
    				$mongo = new Mongo($db_info['dsn'], array('timeout'=>1000000));
    			}
    		}
    		catch (MongoConnectionException $e) {
    			throw_exception('连接Mongodb服务器失败：'.$db_info['dsn']);
    		}

    		$this->conn_list[$db_info['link_key']] = $mongo;

    		$this->mongo = $mongo;

	    	// 标记连接成功
	    	$this->connected = true;
    	}

    	if ($this->last_db != $db_info['db']) {
    		$this->db = $this->mongo->selectDB($db_info['db']);
    		$this->last_db = $db_info['db'];

    		$this->collection = $this->db->selectCollection($db_info['collection']);
			$this->last_collection = $db_info['collection'];
    	}
		elseif ($this->last_collection != $db_info['collection']) {
			$this->collection = $this->db->selectCollection($db_info['collection']);
			$this->last_collection = $db_info['collection'];
		}

    	return true;
    }

    /**
     * 插入一条记录
     * 需要sets字段
     */
    public function insert($data='') {

    	if ((!empty($data)) && is_array($data)) {
    		$this->data = $data;
    	}

    	/// 连接mongodb服务器
    	$this->connect();

    	/// 判断data['sets']是否存在，并且是一个数组
    	if (!(isset($this->data['sets']) && is_array($this->data['sets']))) {
    		throw_exception('Mongodb插入操作，data[\'sets\']必须存在，且是数组形式！'.var_export($this->data, true));
    	}

    	$ret = $this->collection->insert($this->data['sets'], array('safe'=>true));

    	$this->error = $ret['err'];

    	$ret['ok'] = intval($ret['ok']);

    	if ($ret['ok']) return true;

    	return false;
    }

    /**
     * 更新记录，支持批量更新
     * 需要sets字段
     * where字段可选
     *
     * @param mixed 返回更新的数量，失败返回false
     * @param boolean upsert, default:false
     */
    public function update($data='', $upsert=false) {

    	if ((!empty($data)) && is_array($data)) {
    		$this->data = $data;
    	}

    	/// 连接mongodb服务器
    	$this->connect();

    	/// 判断data['sets']是否存在，并且是一个数组
    	if (!(isset($this->data['sets']) && is_array($this->data['sets']))) {
    		throw_exception('Mongodb更新操作，data[\'sets\']必须存在，且是数组形式！');
    	}

    	if (!isset($this->data['where'])) $this->data['where'] = array();
    	elseif (!is_array($this->data['where'])) {
    		throw_exception('Mongodb更新操作，data[\'where\']必须是数组！');
    	}

    	$options = array(
			'multiple'=>true,
			'safe'=>true
		);

		if ($upsert) {
			$options['upsert'] = true;
		}
    	$ret = $this->collection->update($this->data['where'], $this->data['sets'], $options);

    	$this->error = $ret['err'];

    	$ret['ok'] = intval($ret['ok']);
    	$ret['n']  = intval($ret['n']);

    	if ($ret['ok']) return $ret['n'];

    	return false;
    }

    /**
     * 删除记录
     * where字段可选
     */
    public function delete($data='') {

    	if ((!empty($data)) && is_array($data)) {
    		$this->data = $data;
    	}

    	/// 连接mongodb服务器
    	$this->connect();

    	if (!isset($this->data['where'])) $this->data['where'] = array();
    	elseif (!is_array($this->data['where'])) {
    		throw_exception('Mongodb更新操作，data[\'where\']必须是数组！');
    	}

    	$ret = $this->collection->remove($this->data['where'], array('safe'=>true));

    	$this->error = $ret['err'];

    	$ret['ok'] = intval($ret['ok']);
    	$ret['n']  = intval($ret['n']);

    	if ($ret['ok']) return $ret['n'];

    	return false;
    }

    /**
     * 读取一条记录
     *
     * @param array data
     * 	array(
     * 		'fields'  => array(...), /// 可选，不设置或者为空(空数组)表示返回全部字段
     * 		'where'   => array(...), /// 条件
     * 	)
     *
     * fields, where都是可选
     */
    public function getRow($data='') {

    	if ((!empty($data)) && is_array($data)) {
    		$this->data = $data;
    	}

    	$this->data = $this->parse_data($this->data);

    	/// 连接mongodb服务器
    	$this->connect();

    	$ret = $this->collection->findOne($this->data['where'], $this->data['fields']);

    	if (is_null($ret)) return false;

    	return $ret;
    }

    /**
     * 读取记录列表
     *
     * @param array data
     * 	array(
     * 		'fields'  => array(...), /// 可选，不设置或者为空(空数组)表示返回全部字段
     * 		'where'   => array(...), /// 条件
     * 		'orderby' => array('id'=>'asc', 'update_time'=>'desc'), /// 排序条件
     * 		'limit'   => array(0, 10), /// limit 开始位置，pagesize，如果是array(10)，表示0,10
     * 	)
     *
     * fields, where, orderby, limit都是可选
     */
    public function getAssoc($data='') {

    	if ((!empty($data)) && is_array($data)) {
    		$this->data = $data;
    	}

    	$this->data = $this->parse_data($this->data);

    	/// 连接mongodb服务器
    	$this->connect();

    	$cursor = $this->collection->find($this->data['where'], $this->data['fields']);

    	if (isset($this->data['limit'])) {
    		$cursor->limit($this->data['limit']['limit']);
    		if ($this->data['limit']['skip'] > 0) {
    			$cursor->skip($this->data['limit']['skip']);
    		}
    	}

    	if (isset($this->data['orderby'])) {
    		$cursor->sort($this->data['orderby']);
    	}

    	$ret = array();
    	while ($cursor->hasNext()) {
			$ret[] = $cursor->getNext();
		}

		return $ret;
    }

    /**
     * 得到列表的总数，相当于MySQL的select count(*)
     * 不支持limit
     *
     * @param array data
     * 	array(
     * 		'where'   => array(...), /// 条件
     * 	)
     */
    public function getCount($data='') {

    	if ((!empty($data)) && is_array($data)) {
    		$this->data = $data;
    	}

    	$this->data = $this->parse_data($this->data);

    	/// 连接mongodb服务器
    	$this->connect();

    	$ret = $this->collection->count($this->data['where']);

    	return $ret;
    }

    /**
     * 解析select的data
     *
     * @param array data
     */
    private function parse_data($data) {

    	if (!is_array($data)) return array();

    	if ((!isset($data['fields'])) || empty($data['fields'])) $data['fields'] = array();
    	elseif (!is_array($data['fields'])) {
    		throw_exception('Mongodb select操作fields语法不正确，必须是数组或者为空！');
    	}

    	if ((!isset($data['where'])) || empty($data['where'])) $data['where'] = array();
    	elseif (!is_array($data['where'])) {
    		throw_exception('Mongodb select操作where语法不正确，必须是数组或者为空！');
    	}

    	if (isset($data['orderby']) && is_array($data['orderby'])) {

    		foreach ($data['orderby'] as $k=>$v) {

    			if (strtolower(strval($v)) == 'asc') {
    				$data['orderby'][$k] = 1;
    			}
    			elseif (strtolower(strval($v)) == 'desc') {
    				$data['orderby'][$k] = -1;
    			}
    			else {
    				throw_exception('Mongodb select操作orderby语法不正确data[\'orderby\']=array(\'record_time\'=>\'asc\', \'update_time\'=>\'desc\')！');
    			}
    		}
    	}
    	else {
    		unset($data['orderby']);
    	}

    	if (isset($data['limit']) && is_array($data['limit'])) {

    		$c = count($data['limit']);

    		if ($c > 2) {
    			throw_exception('Mongodb select操作limit语法不正确！');
    		}

    		foreach ($data['limit'] as $k=>$v) {
    			$data['limit'][$k] = intval($v);
    		}

    		if ($c == 1) {
    			$tmp = array();
    			$tmp['limit'] = $data['limit'][0];
    			$tmp['skip']  = 0;
    			$data['limit'] = $tmp;
    		}
    		else {
    			$tmp = array();
    			$tmp['limit'] = $data['limit'][1];
    			$tmp['skip']  = $data['limit'][0];
    			$data['limit'] = $tmp;
    		}
    	}
    	else {
    		unset($data['limit']);
    	}

    	return $data;
    }

    /**
     * 解析成Mongodb的配置信息形式
     *
     * @param array cfg
     * 		array(
     * 			'dsn'        => 'mongodb://root:fdafd@10.245.75.75:65432',
     * 			'db'         => 'db2_user_0_0',
     * 			'collection' => 't_user_0',
     * 		)
     */
    private function parse_dbcfg($cfg) {

    	$ret = array();

    	if (empty($cfg['USERNAME']) && empty($cfg['PASSWORD'])) {
    		$ret['dsn'] = 'mongodb://'.$cfg['SERVER'];
    	}
    	else {
    		$ret['dsn'] = 'mongodb://'.$cfg['USERNAME'].':'.$cfg['PASSWORD'].'@'.$cfg['SERVER'];
    	}

    	$tmp = explode('.', $cfg['DBTABLE']);

    	$ret['db'] = $tmp[0];
    	$ret['collection'] = $tmp[1];

    	return $ret;
    }
}
?>